package th.co.yellowpages.javame;
/*
 * http://code.google.com/p/qrcoder/source/browse/blackberry/src/th/co/yellowpages/javame/PNGEncoder.java?r=5d2bb29f4a9d85c3fbddda545a43434571f0af4f
 * http://www.developer.nokia.com/Community/Wiki/PNG_Encoding_in_Java_ME
 */
/*
 * Minimal PNG encoder to create PNG streams (and MIDP images) from RGBA arrays.
 * 
 * Copyright 2006-2009 Christian Fröschlin
 * 
 * www.chrfr.de
 * 
 * 
 * Changelog:
 * 
 * 09/22/08: Fixed Adler checksum calculation and byte order for storing length
 * of zlib deflate block. Thanks to Miloslav Ruzicka for noting this.
 * 
 * 05/12/09: Split PNG and ZLIB functionality into separate classes. Added
 * support for images > 64K by splitting the data into multiple uncompressed
 * deflate blocks.
 * 
 * 03/19/10: Re-packaged, and modified interface to be more MIDP-2 friendly,
 * using int[] rather than byte[] data.  Alpha channel is now optional, to
 * allow a smaller output size.  New toPNG() method works directly from an
 * Image object. (Graham Hughes, for Forum Nokia)
 * 
 * Terms of Use:
 * 
 * You may use the PNG encoder free of charge for any purpose you desire, as
 * long as you do not claim credit for the original sources and agree not to
 * hold me responsible for any damage arising out of its use.
 * 
 * If you have a suitable location in GUI or documentation for giving credit,
 * I'd appreciate a mention of
 * 
 * PNG encoder (C) 2006-2009 by Christian Fröschlin, www.chrfr.de
 * 
 * but that's not mandatory.
 * 
 */

import com.jcraft.jzlib.JZlib;
import com.jcraft.jzlib.ZOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import javax.microedition.lcdui.Image;

public class PNGEncoder {

    private static final byte[] SIGNATURE = new byte[]{(byte) 137, (byte) 80,
        (byte) 78, (byte) 71, (byte) 13, (byte) 10, (byte) 26, (byte) 10};

    /**
     * Generate a PNG data stream from a pixel array. <p> Setting processAlpha
     * to false will result in a PNG file that contains no transparency
     * information, but may be up to 25% smaller. <p> The pixel array must
     * contain (width * height) pixels.
     *
     * @param width width of image, in pixels
     * @param height height of image, in pixels
     * @param argb pixel array, as populated from Image.getRGB()
     * @param processAlpha true if you want to keep alpha channel data
     * @return PNG data in a byte[]
     * @throws IllegalArgumentException if the size of the pixel array does not
     * match the specified width and height
     */
    public static byte[] toPNG(int width, int height, int[] argb,
            boolean processAlpha) throws IllegalArgumentException {
        ByteArrayOutputStream png;
        try {
            byte[] header = createHeaderChunk(width, height, processAlpha);
            byte[] data = createDataChunk(width, height, argb, processAlpha);
            byte[] trailer = createTrailerChunk();

            png = new ByteArrayOutputStream(SIGNATURE.length + header.length
                    + data.length + trailer.length);
            png.write(SIGNATURE);
            png.write(header);
            png.write(data);
            png.write(trailer);
        } catch (IOException ioe) {
            // none of the code should ever throw an IOException
            throw new IllegalStateException("Unexpected " + ioe);
        }
        return png.toByteArray();
    }

    /**
     * Generate a PNG data stream from an Image object.
     *
     * @param img source Image
     * @param processAlpha true if you want to keep the alpha channel data
     * @return PNG data in a byte[]
     */
    public static byte[] toPNG(Image img, boolean processAlpha) {
        int width = img.getWidth();
        int height = img.getHeight();
        int[] argb = new int[width * height];
        img.getRGB(argb, 0, width, 0, 0, width, height);
        // allow garbage collection, if this is the only reference
        img = null;
        return toPNG(width, height, argb, processAlpha);
    }

    private static byte[] createHeaderChunk(int width, int height,
            boolean processAlpha) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream(13);
        DataOutputStream chunk = new DataOutputStream(baos);
        chunk.writeInt(width);
        chunk.writeInt(height);
        chunk.writeByte(8); // Bitdepth
        chunk.writeByte(processAlpha ? 6 : 2); // Colortype ARGB or RGB
        chunk.writeByte(0); // Compression
        chunk.writeByte(0); // Filter
        chunk.writeByte(0); // Interlace
        return toChunk("IHDR", baos.toByteArray());
    }

    private static byte[] createDataChunk(int width, int height, int[] argb,
            boolean processAlpha) throws IOException, IllegalArgumentException {
        if (argb.length != (width * height)) {
            throw new IllegalArgumentException(
                    "array size does not match image dimensions");
        }
        int source = 0;
        int dest = 0;
        byte[] raw = new byte[(processAlpha ? 4 : 3) * (width * height)
                + height];
        for (int y = 0; y < height; y++) {
            raw[dest++] = 0; // No filter
            for (int x = 0; x < width; x++) {
                int pixel = argb[source++];
                raw[dest++] = (byte) (pixel >> 16); // red
                raw[dest++] = (byte) (pixel >> 8); // green
                raw[dest++] = (byte) (pixel); // blue
                if (processAlpha) {
                    raw[dest++] = (byte) (pixel >> 24); // alpha
                }
            }
        }
        return toChunk("IDAT", toZLIB(raw));
    }

    private static byte[] createTrailerChunk() throws IOException {
        return toChunk("IEND", new byte[]{});
    }

    private static byte[] toChunk(String id, byte[] raw) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream(raw.length + 12);
        DataOutputStream chunk = new DataOutputStream(baos);

        chunk.writeInt(raw.length);

        byte[] bid = new byte[4];
        for (int i = 0; i < 4; i++) {
            bid[i] = (byte) id.charAt(i);
        }

        chunk.write(bid);

        chunk.write(raw);

        int crc = 0xFFFFFFFF;
        crc = updateCRC(crc, bid);
        crc = updateCRC(crc, raw);
        chunk.writeInt(~crc);

        return baos.toByteArray();
    }
    private static int[] crcTable = null;

    private static void createCRCTable() {
        crcTable = new int[256];

        for (int i = 0; i < 256; i++) {
            int c = i;
            for (int k = 0; k < 8; k++) {
                c = ((c & 1) > 0) ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
            }
            crcTable[i] = c;
        }
    }

    private static int updateCRC(int crc, byte[] raw) {
        if (crcTable == null) {
            createCRCTable();
        }

        for (int i = 0; i < raw.length; i++) {
            crc = crcTable[(crc ^ raw[i]) & 0xFF] ^ (crc >>> 8);
        }

        return crc;
    }

    /*
     * This method is called to encode the image data as a zlib block as
     * required by the PNG specification. This file comes with a minimal ZLIB
     * encoder which uses uncompressed deflate blocks (fast, short, easy, but no
     * compression). If you want compression, call another encoder (such as
     * JZLib?) here.
     */
    private static byte[] toZLIB(byte[] raw) throws IOException {
        return ZLIB.toZLIB(raw);
    }
}

class ZLIB {

    private static final int BLOCK_SIZE = 32000;

    //todo: 1 apply compress to the raw data.
    public static byte[] toZLIB_old(byte[] raw) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream(raw.length + 6
                + (raw.length / BLOCK_SIZE) * 5);
        DataOutputStream zlib = new DataOutputStream(baos);

        byte tmp = (byte) 8;
        zlib.writeByte(tmp); // CM = 8, CMINFO = 0
        zlib.writeByte((31 - ((tmp << 8) % 31)) % 31); // FCHECK(FDICT/FLEVEL=0)

        int pos = 0;
        while (raw.length - pos > BLOCK_SIZE) {
            writeUncompressedDeflateBlock(zlib, false, raw, pos,
                    (char) BLOCK_SIZE);
            pos += BLOCK_SIZE;
        }

        writeUncompressedDeflateBlock(zlib, true, raw, pos,
                (char) (raw.length - pos));

        // zlib check sum of uncompressed data
        zlib.writeInt(calcADLER32(raw));

        return baos.toByteArray();
    }

    public static byte[] toZLIB(byte[] raw2) throws IOException {
        byte[] raw = zLib(raw2);
        ByteArrayOutputStream baos = new ByteArrayOutputStream(raw.length + 6
                + (raw.length / BLOCK_SIZE) * 5);
        DataOutputStream zlib = new DataOutputStream(baos);

        byte tmp = (byte) 8;
        zlib.writeByte(tmp); // CM = 8, CMINFO = 0
        zlib.writeByte((31 - ((tmp << 8) % 31)) % 31); // FCHECK(FDICT/FLEVEL=0)

        int pos = 0;
        while (raw.length - pos > BLOCK_SIZE) {
            writeUncompressedDeflateBlock(zlib, false, raw, pos,
                    (char) BLOCK_SIZE);
            pos += BLOCK_SIZE;
        }

        writeUncompressedDeflateBlock(zlib, true, raw, pos,
                (char) (raw.length - pos));

        // zlib check sum of uncompressed data
        zlib.writeInt(calcADLER32(raw));

        return baos.toByteArray();
    }

    private static byte[] zLib(byte[] bContent) throws IOException {

        byte[] data = null;
        try {
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            ZOutputStream zOut = new ZOutputStream(out,
                    JZlib.Z_BEST_COMPRESSION); // 压缩级别,缺省为1级  
            DataOutputStream objOut = new DataOutputStream(zOut);
            objOut.write(bContent);
            objOut.flush();
            zOut.close();
            data = out.toByteArray();
            out.close();
        } catch (IOException e) {
            e.printStackTrace();
            throw e;
        }
        return data;
    }

    private static void writeUncompressedDeflateBlock(DataOutputStream zlib,
            boolean last, byte[] raw, int off, char len) throws IOException {
        zlib.writeByte((byte) (last ? 1 : 0)); // Final flag, Compression type 0
        zlib.writeByte((byte) (len & 0xFF)); // Length LSB
        zlib.writeByte((byte) ((len & 0xFF00) >> 8)); // Length MSB
        zlib.writeByte((byte) (~len & 0xFF)); // Length 1st complement LSB
        zlib.writeByte((byte) ((~len & 0xFF00) >> 8)); // Length 1st complement
        // MSB
        zlib.write(raw, off, len); // Data
    }

//    private static void writeCompressedDeflateBlock(DataOutputStream zlib,
//            boolean last, byte[] raw, int off, char len) throws IOException {
//
//        zlib.writeByte((byte) (last ? 1 : 0)); // Final flag, Compression type 0
//        zlib.writeByte((byte) (len & 0xFF)); // Length LSB
//        zlib.writeByte((byte) ((len & 0xFF00) >> 8)); // Length MSB
//        zlib.writeByte((byte) (~len & 0xFF)); // Length 1st complement LSB
//        zlib.writeByte((byte) ((~len & 0xFF00) >> 8)); // Length 1st complement
//        // MSB
//        zlib.write(raw, off, len); // Data
//    }
    private static int calcADLER32(byte[] raw) {
        int s1 = 1;
        int s2 = 0;
        for (int i = 0; i < raw.length; i++) {
            int abs = raw[i] >= 0 ? raw[i] : (raw[i] + 256);
            s1 = (s1 + abs) % 65521;
            s2 = (s2 + s1) % 65521;
        }
        return (s2 << 16) + s1;
    }
}